Skip to content

feat(libkernel): support microsecond timeout in size-0 _umtx_op - #5887

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
leorossi2005:feat/libkernel-umtx-microsecond-timeout
Oct 16, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
leorossi2005:feat/libkernel-umtx-microsecond-timeout

Conversation

@leorossi2005

Copy link
Copy Markdown
Contributor

What

When _umtx_op is called with a size argument of 0 (sizeArgument == nullptr), UmtxDeadline previously read the timeout pointer as a 16-byte KernelTimespec and threw NotImplemented_nid_no_patch("_umtx_op timeout with size argument 0 that is not a valid timespec") whenever the nanoseconds field fell outside [0, 1e9) or seconds were negative (introduced in #1482).

Decompilation of the caller in Metal Gear Solid 4 (PPSA18194, eboot.bin at 0x1ac2a50 invoking PS5Util.prx's zlqfTyrQSPk) reveals that the caller allocates a 4-byte local variable on the stack storing the timeout in microseconds:

uint uStack_3c = param_2 * 1000;
zlqfTyrQSPk(param_1, 0, &uStack_3c, 0); // invokes _umtx_op WAIT with sizeArgument = 0

Because the stack allocation is only 4 bytes, reading 16 bytes treats 12 bytes of adjacent stack frame data as tv_nsec, causing _umtx_op to reject it as an invalid timespec and throw.

This change handles size-0 timeouts whose fields do not form a valid timespec by reading them as a 4-byte uint32_t microsecond value and converting them to seconds and nanoseconds. This resolves the technical debt noted in #1482 and allows titles using Unity's PS5Util.prx and SCE libc.prx to wait on the intended duration without throwing.

Tested

  • Windows 11 x86-64, Metal Gear Solid 4 (PPSA18194). Previously crashed during startup with _umtx_op timeout with size argument 0 that is not a valid timespec not implemented. With this change, _umtx_op successfully waits on the timeout and the game proceeds past the sync barrier into the in-engine presentation loop at 60 FPS.
  • guest_umtx_op_tests passed, verifying that a 20ms microsecond timeout with sizeArgument == nullptr correctly waits and times out with GUEST_ETIMEDOUT.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: PPSA18194
  • GPUs tested: n/a
  • AI-assisted: yes

@CnRJay CnRJay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decompilation is useful: the caller stores a 4-byte microsecond count and passes its address with size 0. Choosing the interpretation by whether the 16 bytes happen to form a valid timespec only works when the neighbouring stack bytes are invalid, though, and it fails silently when they aren't.

UmtxDeadline still reads KernelTimespec first: tv_sec is 8 bytes, the 4-byte microsecond count plus the next 4 bytes of the caller's frame, and tv_nsec is the 8 bytes after that. If those neighbours are zero (a common value for padding or a freshly cleared local), the timespec is valid, the new branch never runs, and tv_sec is the microsecond count read as seconds. param_2 * 1000 for a 20 ms wait becomes a 20,000-second wait, roughly 5.5 hours, and the title hangs instead of stopping with a message. Whether it hangs depends on stack contents, so it can differ between runs and compilers. On main it at least throws.

FreeBSD's _umtx_op treats a size argument up to sizeof(struct timespec) as a timespec. So the question is what Sony's kernel does with size 0, or whether zlqfTyrQSPk converts the microseconds before reaching the syscall. Two ways to make it exact:

  • If PS5Util.prx's zlqfTyrQSPk is a wrapper rather than _umtx_op itself, implement or patch the conversion at that boundary, where the 4-byte microsecond argument is part of its own ABI. The kernel path then keeps FreeBSD's rule.
  • If the console's _umtx_op itself reads 4 bytes for size 0, that needs evidence (a PS5 probe, or the kernel's decompiled umtx_copyin_timeout). Then the rule is "size 0 means a 32-bit microsecond count" unconditionally, with no validity heuristic.

Either way, the test should include a microsecond value whose neighbouring bytes are zero, which is the case that goes wrong now.

@leorossi2005
leorossi2005 force-pushed the feat/libkernel-umtx-microsecond-timeout branch from 75760cc to a450219 Compare October 14, 2026 18:36
@leorossi2005

Copy link
Copy Markdown
Contributor Author

@CnRJay I disassembled PS5Util.prx.guest.prx (e.g. from PPSA18194) to inspect zlqfTyrQSPk (RVA 0x102a0):

140010340: mov %rdx, %r8          # arg 5: timeout pointer
140010343: mov %rsi, %rdx         # arg 3: val
140010346: mov $0x2, %esi         # arg 2: UMTX_OP_WAIT (2)
140010232: push $0xffffffffffffffff; pop %r9 # arg 6: -1
140010236: xor %ecx, %ecx         # arg 4: sizeArgument = 0
140010238: jmp *_umtx_op          # tail call directly into _umtx_op

zlqfTyrQSPk is a plain thunk that explicitly clears %ecx and jumps directly into _umtx_op without performing any conversion. Callers in Sony's libc.prx (e.g. call_once) also pass sizeArgument = 0 with 4-byte microsecond timeouts. This confirms that the console's _umtx_op itself treats sizeArgument == 0 as a 32-bit microsecond count.

I have updated the PR following the second approach:

  • Dropped the validity heuristic: when sizeArgument == nullptr, _umtx_op reads a 32-bit microsecond count unconditionally.
  • Added a test case in GuestUmtxOp.cpp where the 4-byte microsecond count is followed by zeroed padding on the stack/struct (zeroNeighbours), ensuring it expires in 20 ms without hanging.
  • Updated the existing failsafe waiters in GuestUmtxOp.cpp to explicitly pass SizeOf(sizeof(failsafe)) when passing a KernelTimespec.
  • Updated docs/dev/debt/libkernel.md accordingly.

AI Assisted: Yes

@CnRJay

CnRJay commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Thanks for the disassembly. It shows zlqfTyrQSPk passes the pointer through without converting it. Together with the MGS4 caller that stores param_2 * 1000 in a 4-byte local, that's good evidence the console's _umtx_op reads a 32-bit microsecond count for size 0. Dropping the heuristic is the right call.

One thing to check before this goes in. The entry on main (from #1482) says PPSA12544's PS5Util.prx passes a timespec. If that call also uses size 0, the unconditional rule now reads the low 32 bits of tv_sec as microseconds. A 1 s wait would silently become a 1 µs wait. Could you check how PPSA12544 calls it (the size argument and what it stores), or run it with this change and confirm its waits are unchanged? If it passes size 16, the entry can just say so. If it really passes a 16-byte timespec with size 0, the two callers contradict each other and one of the two claims needs another look.

Also, the test change removes the checks that a size-0 timeout with too many nanoseconds or negative seconds throws. That's expected now. If no other test covers the explicit-size path, the same two timespecs passed with SizeOf(sizeof(KernelTimespec)) would keep its EINVAL covered.

@leorossi2005
leorossi2005 force-pushed the feat/libkernel-umtx-microsecond-timeout branch from a450219 to 4b34a6d Compare October 14, 2026 20:52
@leorossi2005

Copy link
Copy Markdown
Contributor Author

@CnRJay I inspected PPSA12544's binaries (PS5Util.prx, libc.prx, eboot.bin):

  • PS5Util.prx in PPSA12544 does not actually import _umtx_op (04AjkP0jO9U). Its synchronization imports in the PLT/GOT are sceKernelSyncOnAddressWait (Hc4CaR6JBL0), sceKernelSyncOnAddressWake (q2y-wDIVWZA), and sceKernelSleep (-ZR+hG7aDHw).
  • The only PS5Util.prx that imports and calls _umtx_op is the one in PPSA18194 (via zlqfTyrQSPk), which explicitly passes sizeArgument = 0 with a 4-byte microsecond count.
  • Running PPSA12544 with this change progresses immediately into asset loading and launches all its worker threads (Job.Worker 0..15, Background Job.Worker 0..15, presenter, UnityGfxDeviceWorker) without any synchronization delays or hangs.
  • There is no conflicting caller passing a 16-byte timespec with size 0.

Regarding the tests:

  • Updated RejectsAnInvalidTimeout() in GuestUmtxOp.cpp so that both UMTX_OP_WAIT and UMTX_OP_WAIT_UINT are tested with SizeOf(sizeof(KernelTimespec)) for negative and tooManyNanos, keeping EINVAL fully covered on the explicit-size path.

This was referenced Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 docs/dev/debt/libkernel.md
PR 5280 docs/dev/debt/libkernel.md
PR 5993 docs/dev/debt/libkernel.md
PR 6044 core/libs/tests/GuestUmtxOp.cpp
PR 6222 docs/dev/debt/libkernel.md
PR 6228 docs/dev/debt/libkernel.md
PR 6447 core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp
PR 6510 docs/dev/debt/libkernel.md
PR 6519 docs/dev/debt/libkernel.md
PR 6658 docs/dev/debt/libkernel.md

@isnisk

isnisk commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Checked on Linux: Ubuntu 26.04 (WSL2), GCC 15.2.0, Release, this head (4b34a6d5a) merged onto main 489466d3f (clean merge).

  • With the change: guest_umtx_op and guest_sync_on_address pass, 3 rounds; no compiler warnings.
  • Without it (main's SyncOnAddress.cpp, the new test kept): guest_umtx_op does not finish in 3 of 3 runs; ctest kills it at its 20 s timeout. Here main reads the 4-byte count plus the bytes after it as a timespec, which on this run gives a deadline that does not expire, where MGS4 got the not-a-valid-timespec throw. So the result on main depends on the neighboring stack bytes, as CnRJay pointed out.
  • check_conventions.py --base origin/main passes.

Not checked: Windows, macOS, the title.

AI-assisted: yes (Claude Code).

@CnRJay

CnRJay commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Thanks for checking PPSA12544. If its PS5Util.prx doesn't import _umtx_op at all, the old debt note was about another path, and the unconditional size-0 rule has no conflicting caller. @isnisk's run also confirms the failure mode on main: the new test hangs there, with a deadline that depends on stack contents. Both of my points are covered. Nothing more from me.

@adriadam10 adriadam10 self-assigned this Oct 16, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the build and the libkernel test suite (175 tests green, guest_umtx_op passes), and that the new test fails when the SyncOnAddress.cpp change is reverted. The size-0 read as a 32-bit microsecond count matches the callers disassembled in the thread, and the debt entry is updated; merging.

@adriadam10
adriadam10 merged commit bc9833e into boykopovar:main Oct 16, 2026
8 checks passed
@leorossi2005
leorossi2005 deleted the feat/libkernel-umtx-microsecond-timeout branch October 16, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants